﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Web.Services.Protocols;
using System.Xml;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.Sdk.Query;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.SdkTypeProxy.Metadata;
using OrbitOne.CRM.CRMDiscovery;

namespace OrbitOne.CRM
{
    public class CrmServiceWrapper
    {
        // Define an expired authentication ticket error code.
        private const string ExpiredAuthTicket = "8004A101";
        private const string DiscoveryUrlFormatString = "{0}/MSCRMServices/2007/{1}/CrmDiscoveryService.asmx";
        private const int MaxRetryCount = 3;

        private string m_username;
        private string m_password;
        private string m_server;
        private string m_instance;
        private string m_partner;
        private string m_domain;
        private AuthenticationType m_authType;
        public CrmService Service;
        public MetadataService MetaDataService;

        public CrmServiceWrapper()
        {
            var config = CrmConfiguration.Load();
            m_username = config.Username;
            m_password = config.Password;
            m_server = config.Url;
            m_instance = config.Instance;
            m_domain = config.Domain;
            m_authType = config.AuthenticationType;
            m_partner = null;
            connect();
        }

        public CrmServiceWrapper(string username, string password, string domain, string server, string instance, string partner, AuthenticationType authType)
        {
            m_username = username;
            m_password = password;
            m_server = server;
            m_instance = instance;
            m_domain = domain;
            m_partner = partner;
            m_authType = authType;
            connect();
        }

        public Guid Create(BusinessEntity entity)
        {
            return create(0, entity);
        }
        private Guid create(int retryCount, BusinessEntity entity)
        {
            Guid retval;
            try
            {
                retval = Service.Create(entity);
            }
            catch (SoapException ex)
            {
                // Handle the exception thrown from an expired ticket condition.
                if (getErrorCode(ex.Detail) == ExpiredAuthTicket)
                {
                    if (retryCount < MaxRetryCount)
                    {
                        //Reclaim the authticket
                        connect();
                        // Retry CrmService web service call
                        retval = create(retryCount++, entity);
                    }
                    else
                    {
                        throw;
                    }
                }
                else
                {
                    // If this was some other Soap exception, rethrow the exception.
                    throw;
                }
            }
            return retval;
        }

        public BusinessEntity Retrieve(string entityName, Guid id, ColumnSetBase columnSet)
        {
            return retrieve(0, entityName, id, columnSet);
        }
        private BusinessEntity retrieve(int retryCount, string entityName, Guid id, ColumnSetBase columnSet)
        {
            BusinessEntity ent;
            try
            {
                ent = Service.Retrieve(entityName, id, columnSet);
            }
            catch (SoapException ex)
            {
                // Handle the exception thrown from an expired ticket condition.
                if (getErrorCode(ex.Detail) == ExpiredAuthTicket)
                {
                    if (retryCount < MaxRetryCount)
                    {
                        //Reclaim the authticket
                        connect();
                        // Retry CrmService web service call
                        ent = retrieve(retryCount++, entityName, id, columnSet);
                    }
                    else
                    {
                        throw;
                    }
                }
                else
                {
                    // If this was some other Soap exception, rethrow the exception.
                    throw;
                }
            }
            return ent;
        }

        public BusinessEntityCollection RetrieveMultiple(QueryBase query)
        {
            return retrieveMultiple(0, query);
        }
        private BusinessEntityCollection retrieveMultiple(int retryCount, QueryBase query)
        {
            BusinessEntityCollection ent;
            try
            {
                ent = Service.RetrieveMultiple(query);
            }
            catch (SoapException ex)
            {
                // Handle the exception thrown from an expired ticket condition.
                if (getErrorCode(ex.Detail) == ExpiredAuthTicket)
                {
                    if (retryCount < MaxRetryCount)
                    {
                        //Reclaim the authticket
                        connect();
                        // Retry CrmService web service call
                        ent = retrieveMultiple(retryCount++, query);
                    }
                    else
                    {
                        throw;
                    }
                }
                else
                {
                    // If this was some other Soap exception, rethrow the exception.
                    throw;
                }
            }
            return ent;
        }

        public Microsoft.Crm.SdkTypeProxy.Response Execute(Microsoft.Crm.SdkTypeProxy.Request request)
        {
            return execute(0, request);
        }
        private Microsoft.Crm.SdkTypeProxy.Response execute(int retryCount, Microsoft.Crm.SdkTypeProxy.Request request)
        {
            Microsoft.Crm.SdkTypeProxy.Response retval;
            try
            {
                retval = Service.Execute(request);
            }
            catch (SoapException ex)
            {
                // Handle the exception thrown from an expired ticket condition.
                if (getErrorCode(ex.Detail) == ExpiredAuthTicket)
                {
                    if (retryCount < MaxRetryCount)
                    {
                        //Reclaim the authticket
                        connect();
                        // Retry CrmService web service call
                        retval = execute(retryCount++, request);
                    }
                    else
                    {
                        throw;
                    }
                }
                else
                {
                    // If this was some other Soap exception, rethrow the exception.
                    throw;
                }
            }
            return retval;
        }

        public void Update(BusinessEntity entity)
        {
            update(0, entity);
        }
        private void update(int retryCount, BusinessEntity entity)
        {
            try
            {
              Service.Update(entity);
            }
            catch (SoapException ex)
            {
                // Handle the exception thrown from an expired ticket condition.
                if (getErrorCode(ex.Detail) == ExpiredAuthTicket)
                {
                    if (retryCount < MaxRetryCount)
                    {
                        //Reclaim the authticket
                        connect();
                        // Retry CrmService web service call
                        update(retryCount++, entity);
                    }
                    else
                    {
                        throw;
                    }
                }
                else
                {
                    // If this was some other Soap exception, rethrow the exception.
                    throw;
                }
            }
        }

        public string Fetch(string fetchXml)
        {
            return fetch(0, fetchXml);
        }
        private string fetch(int retryCount, string fetchXml)
        {
            string retval;
            try
            {
                retval = Service.Fetch(fetchXml);
            }
            catch (SoapException ex)
            {
                // Handle the exception thrown from an expired ticket condition.
                if (getErrorCode(ex.Detail) == ExpiredAuthTicket)
                {
                    if (retryCount < MaxRetryCount)
                    {
                        //Reclaim the authticket
                        connect();
                        // Retry CrmService web service call
                        retval = fetch(retryCount++, fetchXml);
                    }
                    else
                    {
                        throw;
                    }
                }
                else
                {
                    // If this was some other Soap exception, rethrow the exception.
                    throw;
                }
            }
            return retval;
        }

        /// <summary>
        /// Returns the error code that is contained in SoapException.Detail.
        /// </summary>
        /// <param name="errorInfo">An XmlNode that contains application specific error information.</param>
        /// <returns>Error code text or empty string.</returns>
        private static string getErrorCode(XmlNode errorInfo)
        {
            var code = errorInfo.SelectSingleNode("//code");
            return code != null ? code.InnerText : "";
        }
        private void connect()
        {
            ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => true;
            var discoveryService = new CrmDiscoveryService
                                       {
                                           Url = String.Format(DiscoveryUrlFormatString, m_authType == AuthenticationType.Passport ? "https://dev.crm.dynamics.com" : m_server, m_authType)
                                       };
            var credentials = new NetworkCredential(m_username, m_password, m_domain);
            RetrieveCrmTicketRequest crmTicketRequest;

            if (m_authType != AuthenticationType.Passport)
            {
                discoveryService.Credentials = credentials;

                var organisationRequest = new RetrieveOrganizationsRequest
                                              {
                                                  UserId = m_domain + "\\" + m_username,
                                                  Password = m_password
                                              };
                var orgResponse = (RetrieveOrganizationsResponse)discoveryService.Execute(organisationRequest);
                var organizationDetail =
                    orgResponse.OrganizationDetails.SingleOrDefault(
                        o => o.OrganizationName.ToUpperInvariant().Equals(m_instance.ToUpperInvariant()));

                if (organizationDetail == null)
                {
                    throw new Exception("Instance (or organization) " + m_instance + " is not found on the CRM server. Please try again with a new instance.");
                }

                crmTicketRequest = new RetrieveCrmTicketRequest
                                       {
                                           OrganizationName = organizationDetail.OrganizationName,
                                           UserId = m_domain + "\\" + m_username,
                                           Password = m_password
                                       };
            }
            else
            {
                var policyRequest = new RetrievePolicyRequest();
                var policyResponse = (RetrievePolicyResponse)discoveryService.Execute(policyRequest);

                var lm = new LogonManager();
                var passportTicket = lm.Logon(m_username, m_password, m_partner, policyResponse.Policy, "Production");//m_instance);
                lm.Dispose();

                crmTicketRequest = new RetrieveCrmTicketRequest
                {
                    OrganizationName = m_instance,
                    PassportTicket = passportTicket
                };
            }

            var crmTicketResponse = (RetrieveCrmTicketResponse)discoveryService.Execute(crmTicketRequest);

            var token = new CrmAuthenticationToken
                            {
                                AuthenticationType = (int)m_authType,
                                OrganizationName = crmTicketResponse.OrganizationDetail.OrganizationName,
                                CrmTicket = crmTicketResponse.CrmTicket
                            };
            MetaDataService = new MetadataService
                                   {
                                       Url = crmTicketResponse.OrganizationDetail.CrmMetadataServiceUrl,
                                       CrmAuthenticationTokenValue = token
                                   };
            Service = new CrmService
                           {
                               Url = crmTicketResponse.OrganizationDetail.CrmServiceUrl,
                               CrmAuthenticationTokenValue = token
                           };
            
            if (m_authType != AuthenticationType.Passport)
            {
                Service.Credentials = credentials;
                MetaDataService.Credentials = credentials;
            }
        }
    }
}